import ij.IJ;
import ij.ImagePlus;
import ij.process.ImageProcessor;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.TimeZone;

import cells.AVNodeVoxelCell;
import cells.AbstractVoxelCell;
import cells.HisBundleVoxelCell;
import cells.MyocardiumVoxelCell;
import cells.PurkinjeVoxelCell;
import cells.TawaraVoxelCell;

/**
*
* Bachelor thesis at the Institute of Biomedical Engineering, 
* located at the UMIT/EWZ campus in Hall in Tyrol. <br><br>
*
* This class is responsible for the creation of the conductor-syste,
* 
* @author Stefan Fuchs
*
*/
public class ConductorSystemCreator implements Runnable, ActionListener, WindowListener{

	////////////////// Constants ///////////////////////////
	
	/** Length of the His-Bundle in mm*/
	private static final int HIS_BUNDLE_LENGTH = 10; 
	
	//int[]
	/** The actuall myocardium Label*/
	private int[] myocardiumLabel = null;
	
	/** Label to store wheter */
	private int[] occupiedCellsLabel = null;
	
	/** Label to store junction points*/
	private boolean[] junctionPointsLabel = null;
	
	/** Point where the His-Bundle "leaves" the AV-Node <br> 
	 * 		 0: x <br>
	 *       1: y <br>
	 *       2: z*/
	private int[] startPoint;
	
	// int
	/** Height of the Image */
	private int height;

	/** Width of the Image */
	private int width;

	/** Amount of Slices in the stack */
	private int size;
	
	/** Min Roi-Prameters Z-Axis */
	private int minRoiZ;

	/** Max Roi-Prameters Z-Axis */
	private int maxRoiZ;
	
	/** Vertical line to the septum */
	private int dyNorm;
	
	/** Vertical line to the septum */
	private int dxNorm;
	
	//Boolean
	private boolean running = false;
	
	// String
	/** The full name of the Image*/
	private String fullName;
	
	//misc
	/** {@link ArrayList} That Contains the av-Node {@link Voxel}s*/
	private ArrayList<Voxel> avNodeVoxels = null; 
	
	/** Arraylist that contains the cell-voxels*/
	private ArrayList<AbstractVoxelCell> cellVoxelList = null;
	
	/** The Creator Thread*/
	private Thread thread = null;
	
	/** Status-Dialog to display segmentation progress */
	private StatusDialog sd;
	
	/** The {@link Modell} */
	private Modell modell;
	
	///////////////////////////////////////////////////////////////
	/////////// Singleton part ////////////////////////////////////
	//////////////////////////////////////////////////////////////
	
	private static ConductorSystemCreator instance = null;
	
	/**
	 * Singleton method
	 * 
	 * @return
	 */
	public static synchronized ConductorSystemCreator getInstance() {
		
		if ( instance == null ) {
			instance = new ConductorSystemCreator();
		}
		
		return instance;
		
	}
	
	/**
	 * constructor
	 */
	private ConductorSystemCreator() {
		//dummy constructor
	}
	
	///////////////////////////////////////////////////////////////
	/////////// End Singleton part ////////////////////////////////
	//////////////////////////////////////////////////////////////
	
	/**
	 * Prepears and starts the creation
	 */
	public void prepearConductorCreation() {
		this.running = true;
		this.thread = new Thread(ConductorSystemCreator.getInstance());
		this.thread.start();
	}
	
	
	@Override
	public void run() {
		
		long start = System.currentTimeMillis();
		this.modell = Modell.getInstance();
		
		getDimensionParameters();
				
		this.sd = new StatusDialog();
		this.sd.getBtnAbort().addActionListener(this);
		this.sd.getJfDialog().addWindowListener(this);
		
		prepearDataStructures();

		startOperation();
		
		markMyocardium();
	
		//FIXME: uncomment to create cells
		createCellVoxels();
		
		visualizeConductorSystem();
		
		exportForAmira();
		
		this.modell.setVoxelCells(this.cellVoxelList);
		
		this.sd.close();
		this.running = false;
		
		long stop = System.currentTimeMillis();
		long time = stop - start;
		SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
		System.out.println("ConductionSystem Duration: " + sdf.format(time));
		
		clearAll();
	}
	
	private void exportForAmira() {
		
		this.sd.setTitle("Exporting for Amira ...");
		this.sd.setStatus("");
		
		SimpleDateFormat sdf = new SimpleDateFormat("HH_mm_ss - dd.MM.yyyy");
		String fileName = "amira " + sdf.format(new Date()) + ".txt";
		
		StringBuffer amiraResult = new StringBuffer();
		BufferedWriter bw;
		
		try{
			bw = new BufferedWriter(new FileWriter(fileName,true));
			
			for ( int i = 0; i < this.occupiedCellsLabel.length; i++ ) {
				amiraResult.append(this.occupiedCellsLabel[i]);
				amiraResult.append("\n");
				
				if ( i % 500 == 0 ) {
					bw.write(amiraResult.toString());
					amiraResult.setLength(0);
				}
			}
			bw.write(amiraResult.toString());
			bw.close();
		}catch(IOException ioe){
			System.out.println("Fehler beim Schreiben der Datei!");
		}
		
		System.gc();
	}
	
	/**
	 * Marks the rest of the myocardium cells which are not part of the conductor system
	 */
	private void markMyocardium() {
		
		int coordinate1D;
		
		int max = 0;
		
		if ( this.startPoint[2] + 2 >= this.size - 1) {
			max = this.size -1;
		} else {
			max = this.startPoint[2] + 2;
		}
		
		for ( int z = 0; z <= ( max ) ; z++) {
			this.sd.setStatus("Marking Myocard: " + ( 100 * z / this.startPoint[2] + 2 ) + "%" );
			
			for ( int y = 0 ; y < this.height ; y++ ) {
				for ( int x = 0 ; x < this.width ; x++ ) {
					
					coordinate1D = (int) ( this.width * ( this.height * z + y ) + x );
					
					if ( this.myocardiumLabel[coordinate1D] > 0 && this.occupiedCellsLabel[coordinate1D] == 0 ) {
						markCell(x, y, z, AbstractVoxelCell.TYPE_MYOCARD);
					}
				}
			}
		}
	}
	
	/**
	 * Creates the arraylist of VoxelCells for the CA
	 */
	private void createCellVoxels() {
		
		int coordinate1D = 0;
		boolean junctionPoint = false;
		AbstractVoxelCell voxelCell = null;
		
		//FIXME:  z < this.size
		for ( int z = 0; z <= ( this.startPoint[2] + 2 ) ; z++) {
			
			this.sd.setStatus("Creating Voxel Cells: " + ( 100 * z / this.startPoint[2] + 3 ) + "%" );
			
			for ( int y = 0 ; y < this.height ; y++ ) {
				for ( int x = 0 ; x < this.width ; x++ ) {
					
					coordinate1D = (int) ( this.width * ( this.height * z + y ) + x );
					
					junctionPoint = this.junctionPointsLabel[coordinate1D];
					
					switch (this.occupiedCellsLabel[coordinate1D]) {
					case AbstractVoxelCell.TYPE_MYOCARD:
						voxelCell = new MyocardiumVoxelCell(x, y, z, coordinate1D, junctionPoint);
						break;
					case AbstractVoxelCell.TYPE_AV_NODE:
						voxelCell = new AVNodeVoxelCell(x, y, z, coordinate1D);
						break;
					case AbstractVoxelCell.TYPE_HIS_BUNDLE:
						voxelCell = new HisBundleVoxelCell(x, y, z, coordinate1D, junctionPoint);
						break;
					case AbstractVoxelCell.TYPE_PURKINJE:
						voxelCell = new PurkinjeVoxelCell(x, y, z, coordinate1D, junctionPoint);
						break;
					case AbstractVoxelCell.TYPE_TAWARA:
						voxelCell = new TawaraVoxelCell(x, y, z, coordinate1D, junctionPoint);
						break;
					default:
						continue;
					}
					
					voxelCell.addNeigbourList(voxelCell.add3DNeumanNeighours(this.width, this.height));
					this.cellVoxelList.add(voxelCell);
				}
			}
		}
		
	}
	
	/**
	 * Start the operation
	 * 
	 */
	private void startOperation() {
		
		this.sd.setTitle("Creating Conductor System ...");
		this.sd.setTime("");
		this.sd.show(true);
		
		int hisLength = (int) Math.round( HIS_BUNDLE_LENGTH / ( (double) this.modell.getVoxelDepth() )); 
		
		boolean corXPosNeg = false;
		boolean corXPosPos = false;
		boolean corYPosNeg = false;
		boolean corYPosPos = false;
		
		int curX = this.startPoint[0];
		int curXmin = 0;
		int curXmax = 0; 
		
		int curY = this.startPoint[1];
		int curYmin = 0;
		int curYmax = 0;
		
		int ctr = 0;
		int lastSliceCtr = 0;
		int coordinate1D;
		
		for ( int z = this.startPoint[2] - 1  ; z >= this.minRoiZ ; z-- ) {
			
			this.sd.setStatus("Creating Conductor System: " +  ( 100 * ( this.startPoint[2] - this.minRoiZ - z ) / ( this.startPoint[2] - this.minRoiZ ))+ "%"  );
			
			//Calculate the middle of the septum
			corXPosNeg = false;
			corXPosPos = false;
			corYPosNeg = false;
			corYPosPos = false;
			
			curXmax = 0;
			curXmin = 0;
			curYmax = 0;
			curYmin = 0;
		
			ctr = 0;
			
			while ( !corXPosNeg || !corXPosPos || !corYPosNeg || !corYPosPos) {
				ctr++;
				
				//Positive 
				coordinate1D = (int) ( this.width * ( this.height * z + curY) + ( curX + ctr) );
				
				if ( !corXPosPos && this.myocardiumLabel[coordinate1D] > 0 ) {
					curXmax++;
				} else {
					corXPosPos = true;
				}
				
				coordinate1D = (int) ( this.width * ( this.height * z + ( curY + ctr )) + curX );
				
				if ( !corYPosPos && this.myocardiumLabel[coordinate1D] > 0 ) {
					curYmax++;
				} else {
					corYPosPos = true;
				}
				
				//Negative
				coordinate1D = (int) ( this.width * ( this.height * z + curY) + ( curX - ctr) );
				
				if ( !corXPosNeg && this.myocardiumLabel[coordinate1D] > 0 ) {
					curXmin++;
				} else {
					corXPosNeg = true;
				}
				
				coordinate1D = (int) ( this.width * ( this.height * z + ( curY - ctr ) ) + curX );
				
				if ( !corYPosNeg && this.myocardiumLabel[coordinate1D] > 0 ) {
					curYmin++;
				} else {
					corYPosNeg = true;
				}
			}
			
			//just some enhancements
			if ( Math.abs( curXmax - curXmin ) > 2 ) {
				if ( curXmax > curXmin ) {
					curX++;
				} 
				if ( curXmax < curXmin ) {
					curX--;
				}
			}
			
			if ( Math.abs( curYmax - curYmin ) > 13 ) {
				if ( curYmax > curYmin ) {
					curY++;
				} 
				if ( curYmax < curYmin){
					curY--;
				}
			}

			//mark the first cells as His-bundle
			if ( ( this.startPoint[2] - 1 - z ) < hisLength ) {
				markCell(curX,curY, z , AbstractVoxelCell.TYPE_HIS_BUNDLE);
			} 
			else if ( ( this.startPoint[2] - 1 - z ) == hisLength ) {
				markCell(curX,curY, z , AbstractVoxelCell.TYPE_HIS_BUNDLE);
				markCell(curX +1 ,curY, z , AbstractVoxelCell.TYPE_HIS_BUNDLE);
				markCell( curX + 2 , curY, z, AbstractVoxelCell.TYPE_TAWARA);
				markCell( curX - 1, curY, z, AbstractVoxelCell.TYPE_TAWARA);
			}
			//The rest in the septum is tawara
			else {
				
				if ( ( curXmax + curXmin ) > 25 && z < 25) {
					lastSliceCtr++;
				}
				
				if ( lastSliceCtr == 6 ) {
					markCell(curX + 2, curY, z, AbstractVoxelCell.TYPE_TAWARA,true);
					markCell(curX - 1 , curY, z, AbstractVoxelCell.TYPE_TAWARA, true);
					
					createPurkinje(curX + 2, curY , z ,true);
					createPurkinje(curX - 1, curY , z ,false);
					
					return;
				}
				
				markCell( curX + 2 , curY, z, AbstractVoxelCell.TYPE_TAWARA);
				markCell( curX - 1, curY, z, AbstractVoxelCell.TYPE_TAWARA);
				
			}
		}
	}
	
	/**
	 * Create the Purkinje system
	 * 
	 * @param x
	 * @param y
	 * @param z
	 * @param positive
	 * 			true: in positive x-direction <br>	
	 * 			false: in negative x-direction
	 */
	private void createPurkinje(int x, int y, int z, boolean positive) {
		int prefix = ( positive == true ) ? ( 1 ) : ( -1 ) ;
		
		int coordinate1D = 0;
		int tmpCoordiante1D = 0;
		
		int curX = x;
		int curY = y;
		int curZ = z;
		
		boolean myoMarker = true;
		boolean voxelOk = true;
		
		//Purkinje
		while ( myoMarker ) {
			tmpCoordiante1D = (int) ( this.width * ( this.height * curZ + curY ) + ( curX + ( prefix * 3 ) ) );
			
			if ( this.myocardiumLabel[tmpCoordiante1D] > 0 ) {
				curX += prefix;
				markCell(curX, curY, z, AbstractVoxelCell.TYPE_PURKINJE);
			} else {
				myoMarker = false;
			}
		}
		
		int dZ = (int) (((this.maxRoiZ - this.minRoiZ) / 3) * 2 );  
		
		for (int zz = curZ ; zz <= dZ ; zz++) {
			
			voxelOk = true;
			
			for ( int yy = - 1 ; yy <= 1 ; yy++ ) {
				for ( int xx = -1 ; xx <= 1 ; xx++ ) {
					
					coordinate1D = (int) ( this.width * ( this.height * ( zz + 1 )  + ( curY + yy )  ) + ( curX + xx ) );
					
					if ( this.myocardiumLabel[coordinate1D] == 0 ) {
						voxelOk = false;
						break;
					}
				}
				
				if ( !voxelOk ) {
					break;
				}
			}
			
			if ( voxelOk ) {
				curX += prefix * this.dxNorm;
				curY += prefix * this.dyNorm;
			}
			
			coordinate1D = (int) ( this.width * ( this.height * ( zz + 1 )  + curY  ) + ( curX + prefix ) );
			if ( myocardiumLabel[coordinate1D] == 0 ) {
				curX -= prefix;
			}
			
			coordinate1D = (int) ( this.width * ( this.height * ( zz + 1 )  + ( curY + prefix )  ) + curX );
			if ( myocardiumLabel[coordinate1D] == 0 ) {
				curY -= prefix;
			}
			
			coordinate1D = (int) ( this.width * ( this.height * ( zz + 1 )  + ( curY - prefix )  ) + curX );
			if ( myocardiumLabel[coordinate1D] == 0 ) {
				curY += prefix;
			}
			
			coordinate1D = (int) ( this.width * ( this.height * ( zz + 1 )  +  curY   ) + curX );
			if ( this.myocardiumLabel[coordinate1D] == 0 ) {
				System.err.println("NOT IN MYOCARD: " + curX + "/" + curY + "/" + (zz+1));
			}
			
			if ( zz == dZ ) {
				markCell(curX, curY, ( zz + 1 ) , AbstractVoxelCell.TYPE_PURKINJE, true);
			} else {
				if ( (zz % 10) == 0 ) {
					markCell(curX, curY, ( zz + 1 ) , AbstractVoxelCell.TYPE_PURKINJE, true);
				} else {
					markCell(curX, curY, ( zz + 1 ) , AbstractVoxelCell.TYPE_PURKINJE);
				}
			}
		}
	}
	
	/**
	 * Visualizes the conductor system 
	 */
	private void visualizeConductorSystem() {
		
		ImagePlus conductorImage = new ImagePlus(this.fullName + " conduction System", this.modell.getImageStack());
		
		ImageProcessor conductorImageProc = conductorImage.getProcessor();
		
		int ctr = 0;
		
		for ( AbstractVoxelCell v : this.cellVoxelList) {
			ctr++;
			this.sd.setStatus("Drawing Voxels: " + ( 100 * ctr / this.cellVoxelList.size()) + "%");
			
			conductorImage.setSlice(v.getZ() + 1 );
			
			if ( v.type == AbstractVoxelCell.TYPE_MYOCARD) {
				conductorImageProc.putPixel(v.getX(), v.getY() , Modell.COLOR_MYOCARD_CELLS.getRGB());
			} else {
				conductorImageProc.putPixel(v.getX(), v.getY() , Modell.COLOR_CONDUCTOR_CELLS.getRGB());
			}
			
		}
		
		conductorImage.setSlice(1);
		
		conductorImage.show();
	}
	
	/**
	 * Initialize necessary data structures
	 */
	private void prepearDataStructures() {
		
		int xmin = this.width ,ymin = this.height;
		int xmax = 0 ,ymax = 0;
		int zmin = this.size;
		
		this.cellVoxelList = new ArrayList<AbstractVoxelCell>();
		this.myocardiumLabel = this.modell.getMyocardiumLabel();
		this.avNodeVoxels = this.modell.getAvNodeVoxels();
		this.occupiedCellsLabel = new int[this.width * this.height * this.size];
		this.junctionPointsLabel = new boolean[this.width * this.height * this.size];
		
		for (int i = 0; i < this.occupiedCellsLabel.length; i++) {
			this.occupiedCellsLabel[i] = 0;
			this.junctionPointsLabel[i] = false;
		}
		
		//mark AV-Node
		for (Voxel v : this.avNodeVoxels ) {
			
			if ( v.getZ() < zmin ) {
				zmin = v.getZ();
			}
			if ( v.getY() > ymax ) {
				ymax = v.getY();
			}
			if ( v.getY() < ymin ) {
				ymin = v.getY();
			}
			if ( v.getX() > xmax ) {
				xmax = v.getX();
			}
			if ( v.getX() < xmin ) {
				xmin = v.getX();
			}
			
			markCell(v.getX(), v.getY(), v.getZ(), AbstractVoxelCell.TYPE_AV_NODE);
		}
		
		//calculate the Point where the 
		this.startPoint = new int[3];
		this.startPoint[0] = (int) ( xmax - ( Math.abs(xmax-xmin) / 2 ));
		this.startPoint[1] = (int) ( ymax - ( Math.abs(ymax-ymin) / 2 ));
		this.startPoint[2] = (int) zmin;
		
		//calculate dy/dx to get the normal
		int coordinate1D = 0;
		int sepXmin = this.height;
		int sepYmin = this.width;
		int sepXmax = 0;
		int sepYmax = 0;
		
		for ( int y = 0 ; y < this.height ; y++) {
			for ( int x = 0 ; x < this.width ; x++) {
				
				coordinate1D = (int) ( this.width * ( this.height * ( zmin + 1) + y ) + x );
				
				if ( this.myocardiumLabel[coordinate1D] == 3 ) {
					
					if ( y > sepYmax ) {
						sepYmax = y;
					}
					if ( x > sepXmax ) {
						sepXmax = x;
					}
					if ( y < sepYmin ) {
						sepYmin = y; 
					}
					if ( x < sepXmin ) {
						sepXmin = x;
					}
				}
			}
		}
		
		int dy = Math.abs(sepXmax - sepXmin);
		int dx = Math.abs(sepYmax - sepYmin);
		
		double quotient = ((double) dy) / ( (double) dx);
		
		//Verticals
		if (quotient > 1 ) {
			this.dyNorm = 1;
			this.dxNorm = (int) Math.round( dy / ( dx * 1.0 ) );
		} else {
			this.dxNorm = 1;
			this.dyNorm = (int) Math.round( dx / ( dy * 1.0 ) );
		}
	}
	
	/**
	 * Marks the Cells as the specified type
	 * 	
	 * @param x
	 * @param y
	 * @param z
	 * @param type
	 */
	private void markCell(int x, int y, int z, int type) {
		markCell(x, y, z, type, false);
	}
	
	/**
	 * Marks the cell as the specified type
	 * 
	 * @param x
	 * @param y
	 * @param z
	 * @param type
	 * @param isJunction
	 */
	private void markCell(int x, int y, int z, int type, boolean isJunction) {
		
		int coordinate1D = (int) ( this.width * (this.height * z + y ) + x );;
		
		this.occupiedCellsLabel[coordinate1D] = type;
		
		if ( isJunction ) {
			this.junctionPointsLabel[coordinate1D] = true;
		}
	}
	
	/**
	 * Clears all memory expensive variables and calls the GC.
	 */
	private void clearAll(){
		
		this.sd = null;
		this.myocardiumLabel = null;
		this.occupiedCellsLabel = null;
		this.avNodeVoxels = null;
		this.junctionPointsLabel = null;
		
		IJ.freeMemory();
		System.gc();
	}
	
	/**
	 * Reads the dimension-Parameters from the model and sets them accordingly
	 */
	private void getDimensionParameters() {
		this.width = this.modell.getWidth();
		this.height = this.modell.getHeight();
		this.size = this.modell.getSize();
		this.minRoiZ = this.modell.getMinRoiZ();
		this.maxRoiZ = this.modell.getMaxRoiZ();
		
	}
	
	///////////////////////////////////////////////////////////////
	/////////// Setters & Getters ////////////////////////////////
	//////////////////////////////////////////////////////////////
	
	/**
	 * Sets the name of the image
	 */
	public void setFullName(String fullName) {
		this.fullName = fullName;
	}

	/**
	 * Indicates if the Segmentation is still running
	 * 
	 * @return true - still running <br>
	 *         false - not running
	 */
	public boolean isRunning() {
		return running;
	}
	
	// /////////////////////////////////////////////

	// ActionListener
	
	@Override
	@SuppressWarnings("deprecation")
	public void actionPerformed(ActionEvent e) {
		if(e.getSource() == this.sd.getBtnAbort()){
			synchronized (this.thread){
				System.gc();
				this.thread.stop();
			}
			this.running = false;
			this.sd.close();	
		}		
	}

	// ///////////////////////////////////////////////

	// Windowlistener
	
	@Override
	public void windowActivated(WindowEvent e) {
		//empty block		
	}

	@Override
	public void windowClosed(WindowEvent e) {
		//empty block		
	}

	@Override
	@SuppressWarnings("deprecation")
	public void windowClosing(WindowEvent e) {
		if(e.getSource() == this.sd.getBtnAbort()){
			synchronized (this.thread){
				System.gc();
				this.thread.stop();
			}
			this.running = false;
			this.sd.close();	
		}		
	}

	@Override
	public void windowDeactivated(WindowEvent e) {
		//empty block		
	}

	@Override
	public void windowDeiconified(WindowEvent e) {
		//empty block		
	}

	@Override
	public void windowIconified(WindowEvent e) {
		//empty block		
	}

	@Override
	public void windowOpened(WindowEvent e) {
		//empty block
	}
}